using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;

namespace CommonTools.Components.Security
{
	/// <summary>
	/// Used by the SimpleHash class to select a specific Hash algorithm (eg. md5, sha1, ...)
	/// </summary>
    public enum SimpleHashAlgorithm
    {
		/// <summary>
		/// MD5
		/// </summary>
        MD5,
		/// <summary>
		/// SHA1
		/// </summary>
		SHA1,
		/// <summary>
		/// SHA256
		/// </summary>
		SHA256,
		/// <summary>
		/// SHA384
		/// </summary>
		SHA384,
		/// <summary>
		/// SHA512
		/// </summary>
		SHA512
    }

    /// <summary>
    /// This class generates and compares hashes using MD5, SHA1, SHA256, SHA384, 
    /// and SHA512 hashing algorithms. Before computing a hash, it appends a
    /// randomly generated salt to the plain text, and stores this salt appended
    /// to the result. To verify another plain text value against the given hash,
    /// this class will retrieve the salt value from the hash string and use it
    /// when computing a new hash of the plain text. Appending a salt value to
    /// the hash may not be the most efficient approach, so when using hashes in
    /// a real-life application, you may choose to store them separately. You may
    /// also opt to keep results as byte arrays instead of converting them into
    /// base64-encoded strings.
    /// </summary>
	/// <remarks>
	/// <![CDATA[
	/// <summary>
	/// Illustrates the use of the SimpleHash class.
	/// </summary>
	///public class SimpleHashTest
	///{
	///	/// <summary>
	///	/// The main entry point for the application.
	///	/// </summary>
	///	[STAThread]
	///	static void Main(string[] args)
	///	{
	///		string password = "myP@5sw0rd";  // original password
	///		string wrongPassword = "password";    // wrong password
	///
	///		string passwordHashMD5 =
	///			   SimpleHash.ComputeHash(password, "MD5", null);
	///		string passwordHashSha1 =
	///			   SimpleHash.ComputeHash(password, "SHA1", null);
	///		string passwordHashSha256 =
	///			   SimpleHash.ComputeHash(password, "SHA256", null);
	///		string passwordHashSha384 =
	///			   SimpleHash.ComputeHash(password, "SHA384", null);
	///		string passwordHashSha512 =
	///			   SimpleHash.ComputeHash(password, "SHA512", null);
	///
	///		Console.WriteLine("COMPUTING HASH VALUES\r\n");
	///		Console.WriteLine("MD5   : {0}", passwordHashMD5);
	///		Console.WriteLine("SHA1  : {0}", passwordHashSha1);
	///		Console.WriteLine("SHA256: {0}", passwordHashSha256);
	///		Console.WriteLine("SHA384: {0}", passwordHashSha384);
	///		Console.WriteLine("SHA512: {0}", passwordHashSha512);
	///		Console.WriteLine("");
	///
	///		Console.WriteLine("COMPARING PASSWORD HASHES\r\n");
	///		Console.WriteLine("MD5    (good): {0}",
	///							SimpleHash.VerifyHash(
	///							password, "MD5",
	///							passwordHashMD5).ToString());
	///		Console.WriteLine("MD5    (bad) : {0}",
	///							SimpleHash.VerifyHash(
	///							wrongPassword, "MD5",
	///							passwordHashMD5).ToString());
	///		Console.WriteLine("SHA1   (good): {0}",
	///							SimpleHash.VerifyHash(
	///							password, "SHA1",
	///							passwordHashSha1).ToString());
	///		Console.WriteLine("SHA1   (bad) : {0}",
	///							SimpleHash.VerifyHash(
	///							wrongPassword, "SHA1",
	///							passwordHashSha1).ToString());
	///		Console.WriteLine("SHA256 (good): {0}",
	///							SimpleHash.VerifyHash(
	///							password, "SHA256",
	///							passwordHashSha256).ToString());
	///		Console.WriteLine("SHA256 (bad) : {0}",
	///							SimpleHash.VerifyHash(
	///							wrongPassword, "SHA256",
	///							passwordHashSha256).ToString());
	///		Console.WriteLine("SHA384 (good): {0}",
	///							SimpleHash.VerifyHash(
	///							password, "SHA384",
	///							passwordHashSha384).ToString());
	///		Console.WriteLine("SHA384 (bad) : {0}",
	///							SimpleHash.VerifyHash(
	///							wrongPassword, "SHA384",
	///							passwordHashSha384).ToString());
	///		Console.WriteLine("SHA512 (good): {0}",
	///							SimpleHash.VerifyHash(
	///							password, "SHA512",
	///							passwordHashSha512).ToString());
	///		Console.WriteLine("SHA512 (bad) : {0}",
	///							SimpleHash.VerifyHash(
	///							wrongPassword, "SHA512",
	///							passwordHashSha512).ToString());
	///	}
	///}
	/// 
	/// ]]>
	/// </remarks>
    public static class SimpleHash
    {
        /// <summary>
        /// Generates a hash for the given plain text value and returns a
        /// base64-encoded result. Before the hash is computed, a random salt
        /// is generated and appended to the plain text. This salt is stored at
        /// the end of the hash value, so it can be used later for hash
        /// verification.
        /// </summary>
        /// <param name="pText">
        /// Plaintext value to be hashed. The function does not check whether
        /// this parameter is null.
        /// </param>
        /// <param name="pHashAlgorithm">
        /// Name of the hash algorithm. Allowed values are: "MD5", "SHA1",
        /// "SHA256", "SHA384", and "SHA512" (if any other value is specified
        /// MD5 hashing algorithm will be used). This value is case-insensitive.
        /// </param>
        /// <param name="saltBytes">
        /// Salt bytes. This parameter can be null, in which case a random salt
        /// value will be generated.
        /// </param>
        /// <returns>
        /// Hash value formatted as a base64-encoded string.
        /// </returns>
        public static string ComputeHash(string pText,
                                         SimpleHashAlgorithm pHashAlgorithm,
                                         byte[] saltBytes)
        {
            // If salt is not specified, generate it on the fly.
            if (saltBytes == null)
            {
                // Define min and max salt sizes.
                int minSaltSize = 4;
                int maxSaltSize = 8;

                // Generate a random number for the size of the salt.
                Random random = new Random();
                int saltSize = random.Next(minSaltSize, maxSaltSize);

                // Allocate a byte array, which will hold the salt.
                saltBytes = new byte[saltSize];

                // Initialize a random number generator.
                RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

                // Fill the salt with cryptographically strong byte values.
                rng.GetNonZeroBytes(saltBytes);
            }

            // Convert plain text into a byte array.
            byte[] pTextBytes = Encoding.UTF8.GetBytes(pText);

            // Allocate array, which will hold plain text and salt.
            byte[] pTextWithSaltBytes =
                    new byte[pTextBytes.Length + saltBytes.Length];

            // Copy plain text bytes into resulting array.
            for (int i = 0; i < pTextBytes.Length; i++)
                pTextWithSaltBytes[i] = pTextBytes[i];

            // Append salt bytes to the resulting array.
            for (int i = 0; i < saltBytes.Length; i++)
                pTextWithSaltBytes[pTextBytes.Length + i] = saltBytes[i];

            // Because we support multiple hashing algorithms, we must define
            // hash object as a common (abstract) base class. We will specify the
            // actual hashing algorithm class later during object creation.
            HashAlgorithm hash;

            // Initialize appropriate hashing algorithm class.
            switch (pHashAlgorithm)
            {
                case SimpleHashAlgorithm.SHA1:
                    hash = new SHA1Managed();
                    break;

                case SimpleHashAlgorithm.SHA256:
                    hash = new SHA256Managed();
                    break;

                case SimpleHashAlgorithm.SHA384:
                    hash = new SHA384Managed();
                    break;

                case SimpleHashAlgorithm.SHA512:
                    hash = new SHA512Managed();
                    break;

                default:
                    hash = new MD5CryptoServiceProvider();
                    break;
            }

            // Compute hash value of our plain text with appended salt.
            byte[] hashBytes = hash.ComputeHash(pTextWithSaltBytes);

            // Create array which will hold hash and original salt bytes.
            byte[] hashWithSaltBytes = new byte[hashBytes.Length +
                                                saltBytes.Length];

            // Copy hash bytes into resulting array.
            for (int i = 0; i < hashBytes.Length; i++)
                hashWithSaltBytes[i] = hashBytes[i];

            // Append salt bytes to the result.
            for (int i = 0; i < saltBytes.Length; i++)
                hashWithSaltBytes[hashBytes.Length + i] = saltBytes[i];

            // Convert result into a base64-encoded string.
            string pCipherText = Convert.ToBase64String(hashWithSaltBytes);

            // Return the result.
            return pCipherText;
        }

        /// <summary>
        /// Compares a hash of the specified plain text value to a given hash
        /// value. Plain text is hashed with the same salt value as the original
        /// hash.
        /// </summary>
        /// <param name="pText">
        /// Plain text to be verified against the specified hash. The function
        /// does not check whether this parameter is null.
        /// </param>
        /// <param name="pHashAlgorithm">
        /// Name of the hash algorithm. Allowed values are: "MD5", "SHA1", 
        /// "SHA256", "SHA384", and "SHA512" (if any other value is specified,
        /// MD5 hashing algorithm will be used). This value is case-insensitive.
        /// </param>
        /// <param name="pCipherText">
        /// Base64-encoded hash value produced by ComputeHash function. This value
        /// includes the original salt appended to it.
        /// </param>
        /// <returns>
        /// If computed hash mathes the specified hash the function the return
        /// value is true; otherwise, the function returns false.
        /// </returns>
        public static bool VerifyHash(string pText,
                                      SimpleHashAlgorithm pHashAlgorithm,
                                      string pCipherText)
        {
            // Convert base64-encoded hash value into a byte array.
            byte[] hashWithSaltBytes = Convert.FromBase64String(pCipherText);

            // We must know size of hash (without salt).
            int hashSizeInBits, hashSizeInBytes;

            switch (pHashAlgorithm)
            {
                case SimpleHashAlgorithm.SHA1:
                    hashSizeInBits = 160;
                    break;

                case SimpleHashAlgorithm.SHA256:
                    hashSizeInBits = 256;
                    break;

                case SimpleHashAlgorithm.SHA384:
                    hashSizeInBits = 384;
                    break;

                case SimpleHashAlgorithm.SHA512:
                    hashSizeInBits = 512;
                    break;

                default:
                    hashSizeInBits = 128;
                    break;
            }

            // Convert size of hash from bits to bytes.
            hashSizeInBytes = hashSizeInBits / 8;

            // Make sure that the specified hash value is long enough.
            if (hashWithSaltBytes.Length < hashSizeInBytes)
                return false;

            // Allocate array to hold original salt bytes retrieved from hash.
            byte[] saltBytes = new byte[hashWithSaltBytes.Length -
                                        hashSizeInBytes];

            // Copy salt from the end of the hash to the new array.
            for (int i = 0; i < saltBytes.Length; i++)
                saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];

            // Compute a new hash string.
            string expectedHashString =
                        ComputeHash(pText, pHashAlgorithm, saltBytes);

            // If the computed hash matches the specified hash,
            // the plain text value must be correct.
            return (pCipherText == expectedHashString);
        }
    }

    
}
